iOS Socket通讯 - BSD Socket

套接字(Socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

Socket描述了一个IP、端口对。它简化了程序员的操作,知道对方的IP以及PORT就可以给对方发送消息,再由服务器端来处理发送的这些消息。所以,Socket一定包含了通信的双发,即客户端(Client)与服务端(server)。

每一个应用或者说服务,都有一个端口。比如DNS的53端口,http的80端口。我们能由DNS请求到查询信息,是因为DNS服务器时时刻刻都在监听53端口,当收到我们的查询请求以后,就能够返回我们想要的IP信息。所以,从程序设计上来讲,应该包含以下步骤:

  1. 服务端利用Socket监听端口;

    服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

  2. 客户端发起连接;

    指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

  3. 服务端返回信息,建立连接,开始通信;

    当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

总结如下:Socket是对TCP/IP协议的封装,Socket本身并不是协议,没有规定计算机应当怎么样传递消息,只是给程序员提供了一个发送消息的调用接口(API),程序员使用这个接口提供的方法,发送与接收消息。通过Socket我们才能使用TCP/IP协议。

iOS网络编程层次结构

Cocoa层是最上层的基于 Objective-C 的 API,比如 URL访问,NSStream,Bonjour,GameKit等,这是大多数情况下我们常用的 API。Cocoa 层是基于 Core Foundation 实现的。

Core Foundation层:因为直接使用Socket需要更多的编程工作,所以苹果对OS层的Socket进行简单的封装以简化编程任务。该层提供了CFNetwork和CFNetServices两个类,其中CFNetwork又是基于CFStream和CFSocket。

OS层:最底层的BSD Socket提供了对网络编程最大程度的控制,但是编程工作也是最多的,但是编程工作也是最多的。因此,苹果建议我们使用 Core Foundation 及以上层的 API 进行编程。关于BSD Socket的介绍请查看它的维基词条,传送门

  • Cocoa层
    • NSURL
    • NSURLRequest
    • NSURLResponse
    • NSURLConnection
    • NSURLSession
    • GameKit
    • WebKit
  • Core Foundation层
    • 基于C语言的CFNetworking和CFNetServices
  • OS层
    • 基于C语言的BSD Socket

iOS提供的Socket网络编程的接口有CFSocket,BSD Socket,比较著名的第三方库是AsyncSocket。

BSD Socket 是UNIX系统中通用的网络接口,它不仅支持各种不同的网络类型,而且也是一种内部进程之间的通信机制。而iOS系统其实本质就是UNIX,所以可以用,但是比较复杂。

CFSocket 是苹果提供给我们的使用Socket的方式。

AsyncSocket 是一个应用比较广泛的开源库。

Socket和HTTP的区别

HTTP是应用层的协议,而Socket是传输层的协议。两者建立在TCP/IP协议之上,其中TCP协议是传输层的协议,IP协议是网络层的协议。

HTTP连接是短连接,客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

HTTP如果要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的做法是即使不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。

Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用 中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。

而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给 客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

Socket的作用是提供点对点的网络通讯,比如我们平时浏览网页,访问服务器,用到的是HTTP通讯,而QQ,网络直播这样的,需要单点对单点,单点对多点这种情况下就会使用到Socket。

Socket位置

Socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用,也就是说,我们并不能直接使用运输层的东西,而是通过使用Socket提供的API去间接使用。下图是Socket所在的位置:

Socket

BSD Socket API

socket 创建并初始化socket,返回该socket的文件描述符,如果描述符为-1表示创建失败。

1
int socketFileDescriptor = socket(int addressFamily, int type, int protocol)
  • addressFamily

    通常参数是IPv4(AF_INET) 或IPv6(AF_INET6)。

  • type

    表示socket的类型,通常是流stream(SOCK_STREAM) 或数据报文datagram(SOCK_DGRAM)。

  • protocol

    参数通常设置为0,以便让系统自动为选择我们合适的协议,对于stream socket来说会是TCP协议(IPPROTO_TCP),而对于datagram来说会是UDP协议(IPPROTO_UDP)。

close 关闭socket。

1
int close(int socketFileDescriptor)

bind 只在服务器端使用,它将socket与特定主机IP地址与端口号绑定,成功绑定返回0,失败返回-1。

1
int bind(int socketFileDescriptor, sockaddr *addressToBind, int addressStructLength)
  • addressToBind 是一个描述要绑定的IP+Port的结构体。
  • addressStructLength 描述这个结构体的长度。

成功绑定之后,根据协议(TCP/UDP)的不同,我们可以对socket进行不同的操作:

  • UDP:因为UDP是无连接的,绑定之后就可以利用UDP socket传送数据了。
  • TCP:而TCP是需要建立端到端连接的,为了建立TCP连接服务器必须调用listen。

listen 只在服务器端使用,设置服务器的缓冲区队列以接收客户端的连接请求。

1
int listen(int socketFileDescriptor, int backlogSize)
  • backlogSize 表示客户端连接请求缓冲区队列的大小。

当调用listen设置之后,服务器等待客户端请求,然后调用下面的accept来接受客户端的连接请求。

accept 只在服务器端使用,接受客户端连接请求并将客户端的网络地址信息保存到clientAddress中。

1
int accept(int socketFileDescriptor, sockaddr *clientAddress, int clientAddressStructLength)
  • sockaddr 结构体

    1
    2
    3
    4
    struct sockaddr {
    unsigned short sa_family;/*addressfamily,AF_xxx*/
    char sa_data[14];/*14bytesofprotocoladdress*/
    };
    • sa_family是通信类型,最常用的值是 “AF_INET”;
    • sa_data14字节,包含套接字中的目标地址和端口信息;
    • sockaddr的缺陷是sa_data把目标地址和端口信息混在一起了。
  • sockaddr_in 结构体

    1
    2
    3
    4
    5
    6
    struct sockaddr_in {
    short sin_family;/*Address */
    unsigned short sin_port;/*Port number*/
    struct in_addr sin_addr;/*IP address*/
    unsigned char sin_zero[8];/*Same size as struct sockaddr*/
    };
    • sin_family 指代协议族,一般来说是AF_INET(地址族)PF_INET(协议族),在socket编程中只能是AF_INET;

    • sin_port 存储端口号,在linux下,端口号的范围0~65535,同时0~1024范围的端口号已经被系统使用或保留。要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字;

    • sin_addr 是用来表示一个32位的IPv4地址的结构体;

      1
      2
      3
      struct in_addr {
      in_addr_t s_addr;
      };
      • in_addr_t 一般为 32位的unsigned int,其字节顺序为网络顺序(network byte ordered),其中每8位代表一个IP地址位中的一个数值。
      • 例如192.168.3.144记为0x9003a8c0,其中 c0=192 ,a8=168,03=3,90=144。打印的时候可以调用inet_ntoa()函数将其转换为char *类型。
  • sin_zero 没有实际意义,是为了让sockaddr与sockaddr_in两个数据结构在内存中保持大小相同而保留的空字节。

  • sockaddr_in和sockaddr是并列的结构,指向sockaddr_in的结构体的指针也可以指向sockaddr的结构体,并代替它。当客户端连接请求被服务器接受之后,客户端和服务器之间的链路就建立好了,两者就可以通信了。

注意:accept是一个阻塞函数,因此在调用时需要放在子线程中,以免主线程卡死。

connect 客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。

1
int connect(int socketFileDescriptor, sockaddr *serverAddress, int serverAddressLength)

当服务器建立好之后,客户端通过调用该接口向服务器发起建立连接请求。

  • 对于UDP来说,该接口是可选的,如果调用了该接口,表明设置了该 UDP socket 默认的网络地址。
  • 对TCP socket来说这就是传说中三次握手建立连接发生的地方。

注意:该接口调用会阻塞当前线程,直到服务器返回。

gethostbyname 使用 DNS 查找特定主机名字对应的 IP 地址。如果找不到对应的 IP 地址则返回 NULL。

1
hostent* gethostbyname(char *hostname)

send 通过socket发送数据,发送成功返回成功发送的字节数,否则返回 -1。

1
int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags)

一旦连接建立好之后,就可以通过send/receive接口发送或接收数据了。UDP socket 也可以调用该接口来接收数据。

receive 从socket中读取数据,读取成功返回成功读取的字节数,否则返回 -1。

1
int receive(int socketFileDescriptor, char *buffer, int bufferLength, int flags)

sendto 只在UDP协议下使用,通过UDP socket 发送数据到特定的网络地址,发送成功返回成功发送的字节数,否则返回 -1。

1
int sendto(int socketFileDescriptor, char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength)

由于 UDP 可以向多个网络地址发送数据,所以可以指定特定网络地址,以向其发送数据。

recvfrom 只在UDP协议下使用,从UDP socket中读取数据,并保存发送者的网络地址信息,读取成功返回成功读取的字节数,否则返回 -1 。

注意:recvfrom是一个阻塞函数,因此在调用时需要放在子线程中,以免主线程卡死。

1
int recvfrom(int socketFileDescriptor, char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength)

由于UDP可以接收来自多个网络地址的数据,所以需要提供额外的参数,以保存该数据的发送者身份。

Socket C/S

Socket通信就是一种确定了端口号的TCP/IP通信,或者说Socket通信与IP通信差别就是端口确定,协议确定。

端口的打开是双方的,在C/S(Client&&Server)结构的TCP连接中不仅仅要注意到S的端(监听的),实际上C也开了一个端口,而C端的端口是动态端口,TCP连接建立的时候,C端的端口会在三次握手结束后确定,动态打开一个,这个端口不受用户/程序员的控制。

Socket C 端书写步骤

  • 创建客户端Socket,例如使用socket(...)
  • 连接到服务器(Socket编程),例如使用connect(...)
  • 发送数据给服务器,例如使用send(...)
  • 接收服务器返回的数据,例如使用receive(...)
  • 关闭Socket : 例如使用close(socketFileDescriptor)

Socket C/S 常见框架

TCP CS框架 UDP CS框架
常见Socket TCP框架 常见Socket UDP框架

参考链接:

iOS开发网络篇—Socket编程 - 牵左手不离

谈谈iOS网络编程之socket编程技术及应用 - 刘玉刚